<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>粒子时钟-完整版</title>
    <style>
        body{margin: 0;overflow: hidden}
        #canvas{
            background-image: url("./images/back.jpg");
            background-size: cover;
            background-position: center;
        }
    </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    const [width,height]=[window.innerWidth,window.innerHeight];
    const canvas=document.getElementById('canvas');
    canvas.width=width;
    canvas.height=height;
    const  ctx=canvas.getContext('2d');

    /*1.建立四张图片作为图像*/
    const imgH=new Image();
    imgH.src='./images/h.png';
    const imgM=new Image();
    imgM.src='./images/m.png';
    const imgS=new Image();
    imgS.src='./images/s.png';
    const imgO=new Image();
    imgO.src='./images/o.png';

    /*2.声明钟表的基本数据*/
    //钟表数字
    const numDt=[
        //0
        [
            1,1,1,1,
            1,0,0,1,
            1,0,0,1,
            1,0,0,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1
        ],
        //1
        [
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1
        ],
        //2
        [
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            1,1,1,1,
            1,0,0,0,
            1,0,0,0,
            1,1,1,1
        ],
        //3
        [
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            1,1,1,1
        ],
        //4
        [
            1,0,0,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1
        ],
        //5
        [
            1,1,1,1,
            1,0,0,0,
            1,0,0,0,
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            1,1,1,1
        ],
        //6
        [
            1,1,1,1,
            1,0,0,0,
            1,0,0,0,
            1,1,1,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1
        ],
        //7
        [
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1,
            0,0,0,1
        ],
        //8
        [
            1,1,1,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1
        ],
        //9
        [
            1,1,1,1,
            1,0,0,1,
            1,0,0,1,
            1,1,1,1,
            0,0,0,1,
            0,0,0,1,
            1,1,1,1
        ],
        //:
        [
            0,0,0,0,
            0,0,0,0,
            0,0,1,0,
            0,0,0,0,
            0,0,1,0,
            0,0,0,0,
            0,0,0,0
        ],
    ];
    //项目数量
    const itemNum=8;
    //项目间隙的数量
    const itemSpNum=itemNum-1;
    //项目间隙的宽度
    const itemSpace=24;
    //粒子尺寸
    const partSize=24;
    //项目的列数，行数
    const [itemColNum,itemRowNum]=[4,7];
    //项目宽度
    const itemWidth=partSize*itemColNum;
    //时钟宽度
    const clockWidth=itemWidth*itemNum+itemSpace*itemSpNum;
    //时钟的高度
    const clockHeight=partSize*itemRowNum;
    //时钟的位置，放在屏幕中间
    const clockPos={
        x:(width-clockWidth)/2,
        y:(height-clockHeight)/5
    };
    //所有粒子的边界
    const edge={left:0,right:width,bottom:height-50};


    /*粒子对象*/
    class Particle{
        constructor(width, height, img) {
            //尺寸
            this.width = width;
            this.height = height;
            //图像
            this.img = img;
            //位置
            this.x = 0;
            this.y = 0;
            //1：新生，0：坠落，2：成型
            this.state=1;
            //父级
            this.parent = null;

            //缩放
            this.scale=0;
            //速度
            this.vx=this.getVx();
            this.vy=0.002;
            //重力
            this.gravity=0.03;
            //弹力
            this.bounce=-0.85;
        }
        //获取x 轴的速度，避免直上直下的弹动
        //vx 取值范围是[-0.5,0.5]，但不能在[-0.15,0.15] 之间
        getVx(){
            let vx=Math.random()-0.5;
            if (Math.abs(vx)<0.15) {
                return this.getVx();
            }else{
                return vx;
            }
        }
        //更新数据
        update(diff){
            //解构状态、尺寸和位置
            const {state,width,height,parent}=this;
            //解构边界
            const {left,right,bottom}=edge;
            if (state===1){
                //如果粒子的状态为新生状态
                //开始生长
                this.scale+=0.01*diff;
                //长到1 就算成型，不要长了
                if (this.scale>1){
                    this.scale=1;
                    this.state=2;
                }
            }else if(state===0){
                //如果粒子的状态为坠落状态
                this.vy+=this.gravity;
                //设置粒子位置
                this.y+=this.vy*diff;
                this.x+=this.vx*diff;
                //底部碰撞检测
                if(this.y+height>bottom){
                    //将粒子位置调整到底部边界之上
                    this.y=bottom-height;
                    //y 值反弹
                    this.vy *= this.bounce;
                }
                //左右碰撞检测
                if (this.x+width<left||this.x>right){
                    //将此元素从父对象的数组中删除
                    parent.remove(this);
                }
            }
        }
        //绘图方法
        draw(ctx){
            //解构位置、尺寸、缩放
            const {x,y,img,scale}=this;
            ctx.save();
            //变换坐标系位置
            ctx.translate(x,y);
            //缩放坐标系
            ctx.scale(scale,scale);
            //绘图
            ctx.drawImage(img,0,0);
            ctx.restore();
        }
    }
    /*粒子粒子发射器*/
    class Gun{
        constructor(width,height,img) {
            //尺寸
            this.width=width;
            this.height=height;
            //图像
            this.img=img;
            //位置
            this.x=0;
            this.y=0;
            //状态 1：粒子发射器的枪膛中有粒子；0：粒子发射器的枪膛中没有粒子。默认没有粒子。
            this._state=0;
            //粒子库
            this.children=[];
        }
        get state(){
            return this._state;
        }
        set state(val){
            //在为state 赋值时，做简单的diff 判断
            if(this._state!==val){
                if(val){
                    //制造一个粒子
                    this.createParticle();
                }else{
                    //粒子仓库中的第一个粒子发射
                    this.children[0].state=0;
                }
                this._state=val;
            }
        }
        /*新建粒子*/
        createParticle(){
            const {x,y,width,height,img,children}=this;
            //实例化粒子对象
            const part=new Particle(width,height,img);
            //粒子位置
            part.x=x;
            part.y=y;
            //粒子父级
            part.parent=this;
            //粒子状态 1：粒子发射器中要有粒子
            part.state = 1;
            //将粒子以前置的方式添加到粒子仓库里
            children.unshift(part);
        }
        /*删除粒子
        * ele 粒子对象
        * */
        remove(ele){
            const {children}=this;
            const index = children.indexOf(ele);
            if (index!==-1) {
                children.splice(index, 1);
            }
        }
        /*更新
        * diff：以毫秒为单位的时间差
        */
        update(diff){
            //遍历粒子仓库中的所有粒子
            this.children.forEach((ele)=>{
                //更新粒子
                ele.update(diff);
            })
        }
        //绘制辅助线
        drawStroke(ctx){
            const {x,y,width,height}=this;
            ctx.save();
            ctx.strokeStyle='#aaa';
            ctx.strokeRect(x,y,width,height);
            ctx.restore();
        }
    }
    /*items 项目集合
    * items=[项目,项目,项目,项目,项目,项目,项目,项目]，其中有8个项目
    * 项目=[4*7=28个粒子发射器]
    * 1个粒子发射器中有0个或多个粒子
    * */
    const items=[];
    //建立八个项目
    for(let i=0;i<itemNum;i++){
        //建立项目
        const item=[];
        //当前项目x 位置
        let curItemX=(itemWidth+itemSpace)*i+clockPos.x;
        //当前项目对应的图片
        let curImg;
        switch (i){
            //小时图
            case 0:
            case 1:
                curImg=imgH;
                break;
            //冒号图
            case 2:
            case 5:
                curImg=imgO;
                break;
            //分钟图
            case 3:
            case 4:
                curImg=imgM;
                break;
            //秒图
            case 6:
            case 7:
                curImg=imgS;
                break;
        }
        //每个项目都是7列，4行。所以遍历列和行，建立粒子发射器。
        for (let r=0;r<itemRowNum;r++){
            //发射器的y 位置
            const partY=partSize*r+clockPos.y;
            //遍历列
            for(let c=0;c<itemColNum;c++){
                //粒子发射器的x 位置
                const partX=partSize*c+curItemX;
                //实例粒子发射器对象
                const gun=new Gun(partSize,partSize);
                //设置粒子发射器的位置
                gun.x=partX;
                gun.y=partY;
                //设置图像
                gun.img = curImg;
                //粒子发射器绘制辅助线
                //part.drawStroke(ctx);
                //将粒子发射器添加到项目中
                item.push(gun);
            }
        }
        //将项目添加到项目集合里
        items.push(item);
    }
    /*基于时钟文字，修改项目中每个粒子发射器的状态的方法
    * n 数字或冒号在numDt数据集合中的位置
    * item 项目
    * */
    function fitNum(n,item){
        numDt[n].forEach((ele,ind)=>{
            item[ind].state=ele;
        })
    }
    /*先示配代表了冒号的两个项目*/
    fitNum(10,items[2]);
    fitNum(10,items[5]);


    /*计时器*/
    let time=new Date();

    /*updateTime() 获取时间差和时、分、秒的方法
    * 此处的时、分、秒是由两个数字组成的数组，如1点=[0,1]，10点=[1,0]
    * */
    function updateTime(){
        //获取时间差
        const now=new Date();
        const diff=now-time;
        time=now;
        //返回时间差和时、分、秒
        return {
            diff,
            hour:parseTime(time.getHours()),
            minute:parseTime(time.getMinutes()),
            second:parseTime(time.getSeconds())
        };
    }
    //解析时间,如30分钟，解析为[3,0]
    function parseTime(t){
        let arr;
        if(t<10){
            arr=[0,t];
        }else{
            const str=t.toString();
            arr=[parseInt(str[0]),parseInt(str[1])]
        }
        return arr;
    }

    /*更新时钟的时间：基于数组类型的时、分、秒更新时钟*/
    function updateClock(hour,minute,second){
        //匹配小时
        fitNum(hour[0],items[0]);
        fitNum(hour[1],items[1]);
        //匹配分钟
        fitNum(minute[0],items[3]);
        fitNum(minute[1],items[4]);
        //匹配秒
        fitNum(second[0],items[6]);
        fitNum(second[1],items[7]);
    }

    //渲染
    !(function render(){
        //获取时间差和时分秒
        const {diff,hour,minute,second}=updateTime();
        //更新时钟的时间
        updateClock(hour,minute,second);
        //清理画布
        ctx.clearRect(0,0,width,height);
        //坠落中的粒子，要最后渲染，避免其被新生的和成型的粒子遮挡
        const dropParts=[];
        //先渲染state 状态不为0 的元素
        items.flat().forEach((part)=>{
            part.update(diff);
            part.children.forEach((ele)=>{
                if (ele.state){
                    ele.draw(ctx);
                }else{
                    dropParts.push(ele);
                }
            })
        });
        //后渲染状态为0 的元素
        dropParts.forEach((ele)=>{
            ele.draw(ctx);
        });
        //请求动画帧
        requestAnimationFrame(render);
    })()
</script>
</body>
</html>
